#!/usr/bin/env python
#
# Author: Bret Taylor (btaylor@gmail.com)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import ply.lex
import ply.yacc
import sys


def parse(value):
    return PARSER.parse(value, lexer=LEXER)


class Node(dict):
    def __init__(self, node_type=None, production=None, **kwargs):
        if "type" in kwargs and kwargs["type"] is None:
            del kwargs["type"]
        dict.__init__(self, **kwargs)
        if production is None or node_type is None:
            frame = sys._getframe(1)
            if production is None:
                production = frame.f_locals["p"]
            if node_type is None:
                node_type = frame.f_code.co_name.split("_")[-1]
        self["node_type"] = node_type
        self["node_position"] = dict(line=production.lineno(1),
                                     char=production.lexspan(1)[0])

    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name)


class ParseError(Exception):
    def __init__(self, message, line_number=None):
        Exception.__init__(self, message)
        self.message = message
        self.line_number = line_number

    def __str__(self):
        if self.line_number is not None:
            return "%d: %s" % (self.message, self.line_number)
        else:
            return self.message


# Lexer

keywords = frozenset([
    "class",
    "extends",
    "function",
    "null",
    "true",
    "false",
    "var",
    "for",
    "while",
    "if",
    "else",
    "in",
    "return",
    "break",
    "continue",
])

tokens = [
    "identifier",
    "number",
    "string",
    "operator",
    "doublestar",
    "assignoperator",
] + list(k.upper() for k in keywords)

literals = "{}(),:;=.[]+-*/!<>"

def t_comment(t):
    r'/\*(.|\n)*?\*/'
    t.lexer.lineno += t.value.count('\n')

def t_inline_comment(t):
    r'//.*'

def t_newline(t):
    r'\n+'
    t.lexer.lineno += t.value.count("\n")

def t_whitespace(t):
    r'\s+'

def t_identifier(t):
    r'[a-zA-Z_][a-zA-Z0-9_]*'
    if t.value in keywords:
        t.type = t.value.upper()
    return t

def t_number(t):
    r'\d+(.\d+)?'
    if "." in t.value:
        t.value = float(t.value)
    else:
        t.value = int(t.value)
    return t

def t_string(t):
    r'(\"[^\"]+\"|\'[^\']+\')'
    t.value = t.value[1:-1]
    return t

def t_operator(t):
    r'==|!=|<=|>=|<<|>>'
    return t

def t_doublestar(t):
    r'\*\*'
    return t

def t_assignoperator(t):
    r'\+=|-=|<<=|>>='
    return t

def t_error(t):
    raise ParseError("Illegal character '%s'" % t.value[0], t.lineno)
    t.lexer.skip(1)

LEXER = ply.lex.lex()


# Parser

precedence = (
    ('nonassoc', '<', '>', 'operator'),
    ('left', '+', '-'),
    ('left', '*', '/'),
    ('right', 'unary_minus', '!'),
)

def p_file(p):
    """
    file : declaration_list
    """
    p[0] = p[1]

def p_declaration_list(p):
    """
    declaration_list : declaration_list function
                     | declaration_list class
                     | declaration_list variabledeclaration
                     |
    """
    if len(p) == 1:
        p[0] = []
    else:
        p[0] = p[1]
        p[0].append(p[2])

def p_class(p):
    """
    class : CLASS identifier '{' declaration_list '}'
    class : CLASS identifier EXTENDS identifier '{' declaration_list '}'
    """
    if len(p) == 6:
        p[0] = Node(name=p[2], members=p[4])
    else:
        p[0] = Node(name=p[2], extends=p[4], members=p[6])

def p_variabledeclaration(p):
    """
    variabledeclaration : VAR variable ';'
                        | VAR variable '=' expression ';'
    """
    if len(p) > 4:
        p[0] = Node(variable=p[2], value=p[4])
    else:
        p[0] = Node(variable=p[2])

def p_function(p):
    """
    function : FUNCTION identifier '(' parameters ')' type '{' statement_list '}'
             | FUNCTION identifier '(' parameters ',' kwarg_parameters ')' type '{' statement_list '}'
             | FUNCTION identifier '(' ')' type '{' statement_list '}'
             | FUNCTION identifier '(' kwarg_parameters ')' type '{' statement_list '}'
    """
    if len(p) == 12:
        p[0] = Node(name=p[2], parameters=p[4], type=p[8], body=p[10], **p[6])
    elif len(p) == 10:
        if isinstance(p[4], dict):
            p[0] = Node(name=p[2], parameters=[], type=p[6], body=p[8], **p[4])
        else:
            p[0] = Node(name=p[2], parameters=p[4], type=p[6], body=p[8])
    else:
        p[0] = Node(name=p[2], parameters=[], type=p[5], body=p[7])

def p_parameters(p):
    """
    parameters : parameters ',' variable
               | variable
    """
    if len(p) == 1:
        p[0] = []
    elif len(p) == 2:
        p[0] = [p[1]]
    else:
        p[0] = p[1]
        p[0].append(p[3])

def p_kwarg_parameters(p):
    """
    kwarg_parameters : '*' identifier
                     | doublestar identifier
                     | '*' identifier ',' doublestar identifier
    """
    if len(p) == 3:
        if p[1] == '*':
            p[0] = dict(args=p[2])
        else:
            p[0] = dict(kwargs=p[2])
    else:
        p[0] = dict(args=p[2], kwargs=p[5])

def p_variable(p):
    """
    variable : identifier type
    """
    p[0] = Node(name=p[1], type=p[1])

def p_statement_list(p):
    """
    statement_list : statement_list statement
                   |
    """
    if len(p) == 1:
        p[0] = []
    else:
        p[0] = p[1]
        p[0].append(p[2])

def p_statement(p):
    """
    statement : variabledeclaration
              | expressionstatement
              | assignment
              | for
              | while
              | if
              | return
              | break
              | continue
    """
    p[0] = p[1]

def p_expressionstatement(p):
    """
    expressionstatement : expression ';'
    """
    p[0] = Node(expression=p[1])

def p_assignment(p):
    """
    assignment : reference '=' expression ';'
               | reference assignoperator expression ';'
    """
    p[0] = Node(lvalue=p[1], operator=p[2], rvalue=p[3])

def p_for(p):
    """
    for : FOR '(' variable IN expression ')' '{' statement_list '}'
        | FOR '(' VAR variable IN expression ')' '{' statement_list '}'
    """
    if len(p) > 9:
        p[0] = Node(variable=p[4], expression=p[6], body=p[9],
                    declaration=True)
    else:
        p[0] = Node(variable=p[3], expression=p[5], body=p[8])

def p_while(p):
    """
    while : WHILE '(' expression ')' '{' statement_list '}'
    """
    p[0] = Node(expression=p[3], body=p[6])

def p_if(p):
    """
    if : IF '(' expression ')' '{' statement_list '}'
       | IF '(' expression ')' '{' statement_list '}' ELSE '{' statement_list '}'
    """
    if len(p) > 10:
        p[0] = Node(expression=p[3], body=p[6], else_body=p[10])
    else:
        p[0] = Node(expression=p[3], body=p[6])

def p_return(p):
    """
    return : RETURN ';'
           | RETURN expression ';'
    """
    if len(p) > 3:
        p[0] = Node(expression=p[2])
    else:
        p[0] = Node()

def p_break(p):
    """
    break : BREAK ';'
    """
    p[0] = Node()

def p_continue(p):
    """
    continue : CONTINUE ';'
    """
    p[0] = Node()

def p_expression(p):
    """
    expression : reference
               | constant
               | unary
               | binary
               | call
               | anonymousfunction
               | comprehension
               | list
               | object
               | '(' expression ')'
    """
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = p[2]

def p_expression_list(p):
    """
    expression_list : expression_list ',' expression
                    | expression
    """
    if len(p) == 1:
        p[0] = []
    elif len(p) == 2:
        p[0] = [p[1]]
    else:
        p[0] = p[1]
        p[0].append(p[3])

def p_constant(p):
    """
    constant : number
             | string
             | keyword_constant
    """
    p[0] = Node(value=p[1])

def p_keyword_constant(p):
    """
    keyword_constant : NULL
                     | TRUE
                     | FALSE
    """
    p[0] = dict(null=None, true=True, false=False)[p[1]]

def p_binary(p):
    """
    binary : expression '+' expression
           | expression '-' expression
           | expression '*' expression
           | expression '/' expression
           | expression '<' expression
           | expression '>' expression
           | expression operator expression
    """
    p[0] = Node(lhs=p[1], operator=p[2], rhs=p[3])

def p_unary(p):
    """
    unary : '!' expression
          | '-' expression %prec unary_minus
    """
    p[0] = Node(operator=p[1], expression=p[2])

def p_call(p):
    """
    call : expression '(' expression_list ')'
         | expression '(' expression_list ',' kwarg_expressions ')'
         | expression '(' ')'
         | expression '(' kwarg_expressions ')'
    """
    if len(p) == 4:
        p[0] = Node(base=p[1], arguments=[])
    elif len(p) == 5:
        if isinstance(p[3], dict):
            p[0] = Node(base=p[1], arguments=[], **p[3])
        else:
            p[0] = Node(base=p[1], arguments=p[3])
    else:
        p[0] = Node(base=p[1], arguments=p[3], **p[5])

def p_kwarg_expressions(p):
    """
    kwarg_expressions : '*' expression
                      | doublestar expression
                      | '*' expression ',' doublestar expression
    """
    if len(p) == 3:
        if p[1] == '*':
            p[0] = dict(args=p[2])
        else:
            p[0] = dict(kwargs=p[2])
    else:
        p[0] = dict(args=p[2], kwargs=p[5])

def p_reference(p):
    """
    reference : identifier
              | expression '.' identifier
              | expression '[' expression ']'
    """
    if len(p) == 2:
        p[0] = Node(identifier=p[1])
    elif len(p) == 4:
        p[0] = Node(base=p[1], identifier=p[3])
    else:
        p[0] = Node(base=p[1], key=p[3])

def p_anonymousfunction(p):
    """
    anonymousfunction : FUNCTION '(' parameters ')' type '{' statement_list '}'
    """
    p[0] = Node(parameters=p[3], type=p[5], body=p[7])

def p_comprehension(p):
    """
    comprehension : '[' expression FOR '(' variable IN expression ')' ']'
                  | '[' expression FOR '(' variable IN expression ')' IF '(' expression ')' ']'
    """
    if len(p) > 8:
        p[0] = Node(expression=p[2], identifier=p[4], list_expression=p[6],
                    conditional_expression=p[8])
    else:
        p[0] = Node(expression=p[2], identifier=p[4], list_expression=p[6])

def p_list(p):
    """
    list : '[' expression_list ']'
         | '[' ']'
    """
    if len(p) == 3:
        p[0] = Node(values=[])
    else:
        p[0] = Node(values=p[2])

def p_object(p):
    """
    object : '{' object_members '}'
    """
    p[0] = Node(members=p[2])

def p_object_members(p):
    """
    object_members : identifier ':' expression ',' object_members
                   | identifier ':' expression
                   |
    """
    if len(p) == 1:
        p[0] = []
    elif len(p) == 4:
        p[0] = [Node(name=p[1], value=p[3], node_type="objectmember")]
    else:
        p[0] = [Node(name=p[1], value=p[3], node_type="objectmember")] + p[5]

def p_type(p):
    """
    type : ':' identifier
         |
    """
    if len(p) > 1:
        p[0] = p[2]
    else:
        p[0] = None

def p_error(p):
    if p:
        raise ParseError("Syntax error at '%s'" % p.value, p.lineno)
    else:
        raise ParseError("Syntax error at EOF")

PARSER = ply.yacc.yacc()
